/*
 * Copyright (C) 2007 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package mobi.intuitit.android.widget;

import java.lang.reflect.Method;
import java.util.ArrayList;

import android.app.PendingIntent;
import android.content.Context;
import android.content.Intent;
import android.content.IntentSender;
import android.graphics.Bitmap;
import android.graphics.PorterDuff;
import android.graphics.Rect;
import android.graphics.drawable.Drawable;
import android.net.Uri;
import android.os.Bundle;
import android.os.Parcel;
import android.os.Parcelable;
import android.text.TextUtils;
import android.view.LayoutInflater;
import android.view.View;
import android.view.View.OnClickListener;
import android.view.ViewGroup;
import android.widget.Chronometer;
import android.widget.ImageView;
import android.widget.ProgressBar;

/**
 * A class that describes a view hierarchy that can be displayed in another
 * process. The hierarchy is inflated from a layout resource file, and this
 * class provides some basic operations for modifying the content of the
 * inflated hierarchy.
 */
public class SimpleRemoteViews implements Parcelable
{
    
    /**
     * The resource ID of the layout file. (Added to the parcel)
     */
    private int mLayoutId;
    
    /**
     * An array of actions to perform on the view tree once it has been inflated
     */
    protected ArrayList<Action> mActions;
    
    /**
     * Exception to send when something goes wrong executing an action
     * 
     */
    public static class ActionException extends RuntimeException
    {
        private static final long serialVersionUID = 4846278673349592851L;
        
        public ActionException(Exception ex)
        {
            super(ex);
        }
        
        public ActionException(String message)
        {
            super(message);
        }
    }
    
    /**
     * Base class for all actions that can be performed on an inflated view.
     * 
     */
    protected abstract static class Action implements Parcelable
    {
        public abstract void apply(View root) throws ActionException;
        
        public int describeContents()
        {
            return 0;
        }
    }
    
    private class SetLayoutSize extends Action
    {
        private static final int TAG = 5;
        public static final int WIDTH = 1;
        public static final int HEIGHT = 2;
        
        private int mMode;
        private int mViewId;
        private int mValue;
        
        public SetLayoutSize(int viewId, int mode, int value)
        {
            mViewId = viewId;
            mValue = value;
            mMode = mode;
        }
        
        public SetLayoutSize(Parcel parcel)
        {
            mViewId = parcel.readInt();
            mMode = parcel.readInt();
            mValue = parcel.readInt();
        }
        
        public void writeToParcel(Parcel dest, int flags)
        {
            dest.writeInt(TAG);
            dest.writeInt(mViewId);
            dest.writeInt(mMode);
            dest.writeInt(mValue);
        }
        
        @Override
        public void apply(View root) throws ActionException
        {
            View target = root.findViewById(mViewId);
            if (target != null)
            {
                ViewGroup.LayoutParams lps = target.getLayoutParams();
                if (mMode == WIDTH)
                    lps.width = mValue;
                else lps.height = mValue;
                target.setLayoutParams(lps);
            }
        }
    }
    
    /**
     * Equivalent to calling a combination of {@link Drawable#setAlpha(int)},
     * {@link Drawable#setColorFilter(int, android.graphics.PorterDuff.Mode)},
     * and/or {@link Drawable#setLevel(int)} on the {@link Drawable} of a given
     * view.
     * <p>
     * These operations will be performed on the {@link Drawable} returned by
     * the target {@link View#getBackground()} by default. If targetBackground
     * is false, we assume the target is an {@link ImageView} and try applying
     * the operations to {@link ImageView#getDrawable()}.
     * <p>
     * You can omit specific calls by marking their values with null or -1.
     */
    private class SetDrawableParameters extends Action
    {
        public SetDrawableParameters(int id, boolean targetBackground, int alpha, int colorFilter, PorterDuff.Mode mode, int level)
        {
            this.viewId = id;
            this.targetBackground = targetBackground;
            this.alpha = alpha;
            this.colorFilter = colorFilter;
            this.filterMode = mode;
            this.level = level;
        }
        
        public SetDrawableParameters(Parcel parcel)
        {
            viewId = parcel.readInt();
            targetBackground = parcel.readInt() != 0;
            alpha = parcel.readInt();
            colorFilter = parcel.readInt();
            boolean hasMode = parcel.readInt() != 0;
            if (hasMode)
            {
                filterMode = PorterDuff.Mode.valueOf(parcel.readString());
            }
            else
            {
                filterMode = null;
            }
            level = parcel.readInt();
        }
        
        public void writeToParcel(Parcel dest, int flags)
        {
            dest.writeInt(TAG);
            dest.writeInt(viewId);
            dest.writeInt(targetBackground ? 1 : 0);
            dest.writeInt(alpha);
            dest.writeInt(colorFilter);
            if (filterMode != null)
            {
                dest.writeInt(1);
                dest.writeString(filterMode.toString());
            }
            else
            {
                dest.writeInt(0);
            }
            dest.writeInt(level);
        }
        
        @Override
        public void apply(View root)
        {
            final View target = root.findViewById(viewId);
            if (target == null)
            {
                return;
            }
            
            // Pick the correct drawable to modify for this view
            Drawable targetDrawable = null;
            if (targetBackground)
            {
                targetDrawable = target.getBackground();
            }
            else if (target instanceof ImageView)
            {
                ImageView imageView = (ImageView) target;
                targetDrawable = imageView.getDrawable();
            }
            
            if (targetDrawable != null)
            {
                // Perform modifications only if values are set correctly
                if (alpha != -1)
                {
                    targetDrawable.setAlpha(alpha);
                }
                if (colorFilter != -1 && filterMode != null)
                {
                    targetDrawable.setColorFilter(colorFilter, filterMode);
                }
                if (level != -1)
                {
                    targetDrawable.setLevel(level);
                }
            }
        }
        
        int viewId;
        boolean targetBackground;
        int alpha;
        int colorFilter;
        PorterDuff.Mode filterMode;
        int level;
        
        public final static int TAG = 3;
    }
    
    /**
     * Equivalent to calling
     * {@link android.view.View#setOnClickListener(android.view.View.OnClickListener)}
     * to launch the provided {@link PendingIntent}.
     */
    protected class SetOnClickPendingIntent extends Action
    {
        public SetOnClickPendingIntent(int id, PendingIntent pendingIntent)
        {
            this.viewId = id;
            this.pendingIntent = pendingIntent;
        }
        
        public SetOnClickPendingIntent(Parcel parcel)
        {
            viewId = parcel.readInt();
            pendingIntent = PendingIntent.readPendingIntentOrNullFromParcel(parcel);
        }
        
        public void writeToParcel(Parcel dest, int flags)
        {
            dest.writeInt(TAG);
            dest.writeInt(viewId);
            pendingIntent.writeToParcel(dest, 0 /* no flags */);
        }
        
        @Override
        public void apply(View root)
        {
            final View target = root.findViewById(viewId);
            if (target != null && pendingIntent != null)
            {
                OnClickListener listener = new OnClickListener() {
                    public void onClick(View v)
                    {
                        // Find target view location in screen coordinates and
                        // fill into PendingIntent before sending.
                        final int[] location = new int[2];
                        v.getLocationOnScreen(location);
                        Rect srcRect = new Rect();
                        srcRect.left = location[0];
                        srcRect.top = location[1];
                        srcRect.right = srcRect.left + v.getWidth();
                        srcRect.bottom = srcRect.top + v.getHeight();
                        
                        final Intent intent = new Intent();
                        intent.setSourceBounds(srcRect);
                        try
                        {
                            // TODO: Unregister this handler if
                            // PendingIntent.FLAG_ONE_SHOT?
                            v.getContext().startIntentSender(pendingIntent.getIntentSender(), intent, Intent.FLAG_ACTIVITY_NEW_TASK,
                                    Intent.FLAG_ACTIVITY_NEW_TASK, 0);
                        }
                        catch (IntentSender.SendIntentException e)
                        {
                            android.util.Log.e("SetOnClickPendingIntent", "Cannot send pending intent: ", e);
                        }
                    }
                };
                target.setOnClickListener(listener);
            }
        }
        
        int viewId;
        PendingIntent pendingIntent;
        
        public final static int TAG = 1;
    }
    
    /**
     * Base class for the reflection actions.
     */
    protected class ReflectionAction extends Action
    {
        static final int TAG = 2;
        
        static final int BOOLEAN = 1;
        static final int BYTE = 2;
        static final int SHORT = 3;
        static final int INT = 4;
        static final int LONG = 5;
        static final int FLOAT = 6;
        static final int DOUBLE = 7;
        static final int CHAR = 8;
        static final int STRING = 9;
        static final int CHAR_SEQUENCE = 10;
        static final int URI = 11;
        static final int BITMAP = 12;
        static final int BUNDLE = 13;
        int viewId;
        String methodName;
        int type;
        Object value;
        
        ReflectionAction(int viewId, String methodName, int type, Object value)
        {
            this(viewId, methodName, type);
            this.value = value;
        }
        
        protected ReflectionAction(int viewId, String methodName, int type)
        {
            this.viewId = viewId;
            this.methodName = methodName;
            this.type = type;
        }
        
        ReflectionAction(Parcel in)
        {
            this.viewId = in.readInt();
            this.methodName = in.readString();
            this.type = in.readInt();
            readValue(in);
        }
        
        protected void readValue(Parcel in)
        {
            switch (this.type)
            {
                case BOOLEAN:
                    this.value = in.readInt() != 0;
                    break;
                case BYTE:
                    this.value = in.readByte();
                    break;
                case SHORT:
                    this.value = (short) in.readInt();
                    break;
                case INT:
                    this.value = in.readInt();
                    break;
                case LONG:
                    this.value = in.readLong();
                    break;
                case FLOAT:
                    this.value = in.readFloat();
                    break;
                case DOUBLE:
                    this.value = in.readDouble();
                    break;
                case CHAR:
                    this.value = (char) in.readInt();
                    break;
                case STRING:
                    this.value = in.readString();
                    break;
                case CHAR_SEQUENCE:
                    this.value = TextUtils.CHAR_SEQUENCE_CREATOR.createFromParcel(in);
                    break;
                case URI:
                    this.value = Uri.CREATOR.createFromParcel(in);
                    break;
                case BITMAP:
                    this.value = Bitmap.CREATOR.createFromParcel(in);
                    break;
                case BUNDLE:
                    this.value = in.readBundle();
                    break;
                default:
                    break;
            }
        }
        
        public void writeToParcel(Parcel out, int flags)
        {
            out.writeInt(getTag());
            out.writeInt(this.viewId);
            out.writeString(this.methodName);
            out.writeInt(this.type);
            writeValue(out, flags);
        }
        
        protected int getTag()
        {
            return TAG;
        }
        
        protected void writeValue(Parcel out, int flags)
        {
            switch (this.type)
            {
                case BOOLEAN:
                    out.writeInt((Boolean) this.value ? 1 : 0);
                    break;
                case BYTE:
                    out.writeByte((Byte) this.value);
                    break;
                case SHORT:
                    out.writeInt((Short) this.value);
                    break;
                case INT:
                    out.writeInt((Integer) this.value);
                    break;
                case LONG:
                    out.writeLong((Long) this.value);
                    break;
                case FLOAT:
                    out.writeFloat((Float) this.value);
                    break;
                case DOUBLE:
                    out.writeDouble((Double) this.value);
                    break;
                case CHAR:
                    out.writeInt((int) ((Character) this.value).charValue());
                    break;
                case STRING:
                    out.writeString((String) this.value);
                    break;
                case CHAR_SEQUENCE:
                    TextUtils.writeToParcel((CharSequence) this.value, out, flags);
                    break;
                case URI:
                    ((Uri) this.value).writeToParcel(out, flags);
                    break;
                case BITMAP:
                    ((Bitmap) this.value).writeToParcel(out, flags);
                    break;
                case BUNDLE:
                    out.writeBundle((Bundle) this.value);
                    break;
                default:
                    break;
            }
        }
        
        private Class<?> getParameterType()
        {
            switch (this.type)
            {
                case BOOLEAN:
                    return boolean.class;
                case BYTE:
                    return byte.class;
                case SHORT:
                    return short.class;
                case INT:
                    return int.class;
                case LONG:
                    return long.class;
                case FLOAT:
                    return float.class;
                case DOUBLE:
                    return double.class;
                case CHAR:
                    return char.class;
                case STRING:
                    return String.class;
                case CHAR_SEQUENCE:
                    return CharSequence.class;
                case URI:
                    return Uri.class;
                case BITMAP:
                    return Bitmap.class;
                case BUNDLE:
                    return Bundle.class;
                default:
                    return null;
            }
        }
        
        @Override
        public void apply(View root)
        {
            final View view = root.findViewById(viewId);
            if (view == null)
            {
                throw new ActionException("can't find view: 0x" + Integer.toHexString(viewId));
            }
            
            Class<?> param = getParameterType();
            if (param == null)
            {
                throw new ActionException("bad type: " + this.type);
            }
            
            Class<?> klass = view.getClass();
            Method method;
            try
            {
                method = klass.getMethod(this.methodName, getParameterType());
            }
            catch (NoSuchMethodException ex)
            {
                throw new ActionException("view: " + klass.getName() + " doesn't have method: " + this.methodName + "(" + param.getName() + ")");
            }
            
            try
            {
                method.invoke(view, getValue(root.getContext()));
            }
            catch (Exception ex)
            {
                throw new ActionException(ex);
            }
        }
        
        protected Object getValue(Context context)
        {
            return this.value;
        }
    }
    
    /**
     * Create a new RemoteViews object that will display the views contained in
     * the specified layout file.
     * 
     * @param packageName
     *            Name of the package that contains the layout resource
     * @param layoutId
     *            The id of the layout resource
     */
    public SimpleRemoteViews(int layoutId)
    {
        mLayoutId = layoutId;
    }
    
    /**
     * Reads a RemoteViews object from a parcel.
     * 
     * @param parcel
     */
    public SimpleRemoteViews(Parcel parcel)
    {
        mLayoutId = parcel.readInt();
        int count = parcel.readInt();
        if (count > 0)
        {
            mActions = new ArrayList<Action>(count);
            for (int i = 0; i < count; i++)
            {
                int tag = parcel.readInt();
                Action act = loadActionFromParcel(tag, parcel);
                if (act != null)
                    mActions.add(act);
                else throw new ActionException("Tag " + tag + " not found");
            }
        }
    }
    
    protected Action loadActionFromParcel(int tag, Parcel parcel)
    {
        switch (tag)
        {
            case SetLayoutSize.TAG:
                return new SetLayoutSize(parcel);
            case SetOnClickPendingIntent.TAG:
                return new SetOnClickPendingIntent(parcel);
            case SetDrawableParameters.TAG:
                return new SetDrawableParameters(parcel);
            case ReflectionAction.TAG:
                return new ReflectionAction(parcel);
            default:
                return null;
        }
    }
    
    public int getLayoutId()
    {
        return mLayoutId;
    }
    
    /**
     * Add an action to be executed on the remote side when apply is called.
     * 
     * @param a
     *            The action to add
     */
    protected void addAction(Action a)
    {
        if (mActions == null)
        {
            mActions = new ArrayList<Action>();
        }
        mActions.add(a);
    }
    
    /**
     * Equivalent to calling View.setVisibility
     * 
     * @param viewId
     *            The id of the view whose visibility should change
     * @param visibility
     *            The new visibility for the view
     */
    public void setViewVisibility(int viewId, int visibility)
    {
        setInt(viewId, "setVisibility", visibility);
    }
    
    /**
     * Equivalent to calling TextView.setText
     * 
     * @param viewId
     *            The id of the view whose text should change
     * @param text
     *            The new text for the view
     */
    public void setTextViewText(int viewId, CharSequence text)
    {
        setCharSequence(viewId, "setText", text);
    }
    
    /**
     * Equivalent to calling ImageView.setImageResource
     * 
     * @param viewId
     *            The id of the view whose drawable should change
     * @param srcId
     *            The new resource id for the drawable
     */
    public void setImageViewResource(int viewId, int srcId)
    {
        setInt(viewId, "setImageResource", srcId);
    }
    
    /**
     * Equivalent to calling ImageView.setImageURI
     * 
     * @param viewId
     *            The id of the view whose drawable should change
     * @param uri
     *            The Uri for the image
     */
    public void setImageViewUri(int viewId, Uri uri)
    {
        setUri(viewId, "setImageURI", uri);
    }
    
    /**
     * Equivalent to calling ImageView.setImageBitmap
     * 
     * @param viewId
     *            The id of the view whose drawable should change
     * @param bitmap
     *            The new Bitmap for the drawable
     */
    public void setImageViewBitmap(int viewId, Bitmap bitmap)
    {
        setBitmap(viewId, "setImageBitmap", bitmap);
    }
    
    /**
     * Equivalent to calling {@link Chronometer#setBase Chronometer.setBase},
     * {@link Chronometer#setFormat Chronometer.setFormat}, and
     * {@link Chronometer#start Chronometer.start()} or {@link Chronometer#stop
     * Chronometer.stop()}.
     * 
     * @param viewId
     *            The id of the view whose text should change
     * @param base
     *            The time at which the timer would have read 0:00. This time
     *            should be based off of
     *            {@link android.os.SystemClock#elapsedRealtime
     *            SystemClock.elapsedRealtime()}.
     * @param format
     *            The Chronometer format string, or null to simply display the
     *            timer value.
     * @param started
     *            True if you want the clock to be started, false if not.
     */
    public void setChronometer(int viewId, long base, String format, boolean started)
    {
        setLong(viewId, "setBase", base);
        setString(viewId, "setFormat", format);
        setBoolean(viewId, "setStarted", started);
    }
    
    /**
     * Equivalent to calling {@link ProgressBar#setMax ProgressBar.setMax},
     * {@link ProgressBar#setProgress ProgressBar.setProgress}, and
     * {@link ProgressBar#setIndeterminate ProgressBar.setIndeterminate}
     * 
     * If indeterminate is true, then the values for max and progress are
     * ignored.
     * 
     * @param viewId
     *            The id of the view whose text should change
     * @param max
     *            The 100% value for the progress bar
     * @param progress
     *            The current value of the progress bar.
     * @param indeterminate
     *            True if the progress bar is indeterminate, false if not.
     */
    public void setProgressBar(int viewId, int max, int progress, boolean indeterminate)
    {
        setBoolean(viewId, "setIndeterminate", indeterminate);
        if (!indeterminate)
        {
            setInt(viewId, "setMax", max);
            setInt(viewId, "setProgress", progress);
        }
    }
    
    /**
     * Equivalent to calling
     * {@link android.view.View#setOnClickListener(android.view.View.OnClickListener)}
     * to launch the provided {@link PendingIntent}.
     * 
     * @param viewId
     *            The id of the view that will trigger the {@link PendingIntent}
     *            when clicked
     * @param pendingIntent
     *            The {@link PendingIntent} to send when user clicks
     */
    public void setOnClickPendingIntent(int viewId, PendingIntent pendingIntent)
    {
        addAction(new SetOnClickPendingIntent(viewId, pendingIntent));
    }
    
    /**
     * @hide Equivalent to calling a combination of
     *       {@link Drawable#setAlpha(int)},
     *       {@link Drawable#setColorFilter(int, android.graphics.PorterDuff.Mode)}
     *       , and/or {@link Drawable#setLevel(int)} on the {@link Drawable} of
     *       a given view.
     *       <p>
     *       You can omit specific calls by marking their values with null or
     *       -1.
     * 
     * @param viewId
     *            The id of the view that contains the target {@link Drawable}
     * @param targetBackground
     *            If true, apply these parameters to the {@link Drawable}
     *            returned by {@link android.view.View#getBackground()}.
     *            Otherwise, assume the target view is an {@link ImageView} and
     *            apply them to {@link ImageView#getDrawable()}.
     * @param alpha
     *            Specify an alpha value for the drawable, or -1 to leave
     *            unchanged.
     * @param colorFilter
     *            Specify a color for a {@link android.graphics.ColorFilter} for
     *            this drawable, or -1 to leave unchanged.
     * @param mode
     *            Specify a PorterDuff mode for this drawable, or null to leave
     *            unchanged.
     * @param level
     *            Specify the level for the drawable, or -1 to leave unchanged.
     */
    public void setDrawableParameters(int viewId, boolean targetBackground, int alpha, int colorFilter, PorterDuff.Mode mode, int level)
    {
        addAction(new SetDrawableParameters(viewId, targetBackground, alpha, colorFilter, mode, level));
    }
    
    /**
     * Equivalent to calling {@link android.widget.TextView#setTextColor(int)}.
     * 
     * @param viewId
     *            The id of the view whose text should change
     * @param color
     *            Sets the text color for all the states (normal, selected,
     *            focused) to be this color.
     */
    public void setTextColor(int viewId, int color)
    {
        setInt(viewId, "setTextColor", color);
    }
    
    /**
     * Call a method taking one boolean on a view in the layout for this
     * RemoteViews.
     * 
     * @param viewId
     *            The id of the view whose text should change
     * @param methodName
     *            The name of the method to call.
     * @param value
     *            The value to pass to the method.
     */
    public void setBoolean(int viewId, String methodName, boolean value)
    {
        addAction(new ReflectionAction(viewId, methodName, ReflectionAction.BOOLEAN, value));
    }
    
    /**
     * Call a method taking one byte on a view in the layout for this
     * RemoteViews.
     * 
     * @param viewId
     *            The id of the view whose text should change
     * @param methodName
     *            The name of the method to call.
     * @param value
     *            The value to pass to the method.
     */
    public void setByte(int viewId, String methodName, byte value)
    {
        addAction(new ReflectionAction(viewId, methodName, ReflectionAction.BYTE, value));
    }
    
    /**
     * Call a method taking one short on a view in the layout for this
     * RemoteViews.
     * 
     * @param viewId
     *            The id of the view whose text should change
     * @param methodName
     *            The name of the method to call.
     * @param value
     *            The value to pass to the method.
     */
    public void setShort(int viewId, String methodName, short value)
    {
        addAction(new ReflectionAction(viewId, methodName, ReflectionAction.SHORT, value));
    }
    
    /**
     * Call a method taking one int on a view in the layout for this
     * RemoteViews.
     * 
     * @param viewId
     *            The id of the view whose text should change
     * @param methodName
     *            The name of the method to call.
     * @param value
     *            The value to pass to the method.
     */
    public void setInt(int viewId, String methodName, int value)
    {
        addAction(new ReflectionAction(viewId, methodName, ReflectionAction.INT, value));
    }
    
    /**
     * Call a method taking one long on a view in the layout for this
     * RemoteViews.
     * 
     * @param viewId
     *            The id of the view whose text should change
     * @param methodName
     *            The name of the method to call.
     * @param value
     *            The value to pass to the method.
     */
    public void setLong(int viewId, String methodName, long value)
    {
        addAction(new ReflectionAction(viewId, methodName, ReflectionAction.LONG, value));
    }
    
    /**
     * Call a method taking one float on a view in the layout for this
     * RemoteViews.
     * 
     * @param viewId
     *            The id of the view whose text should change
     * @param methodName
     *            The name of the method to call.
     * @param value
     *            The value to pass to the method.
     */
    public void setFloat(int viewId, String methodName, float value)
    {
        addAction(new ReflectionAction(viewId, methodName, ReflectionAction.FLOAT, value));
    }
    
    /**
     * Call a method taking one double on a view in the layout for this
     * RemoteViews.
     * 
     * @param viewId
     *            The id of the view whose text should change
     * @param methodName
     *            The name of the method to call.
     * @param value
     *            The value to pass to the method.
     */
    public void setDouble(int viewId, String methodName, double value)
    {
        addAction(new ReflectionAction(viewId, methodName, ReflectionAction.DOUBLE, value));
    }
    
    /**
     * Call a method taking one char on a view in the layout for this
     * RemoteViews.
     * 
     * @param viewId
     *            The id of the view whose text should change
     * @param methodName
     *            The name of the method to call.
     * @param value
     *            The value to pass to the method.
     */
    public void setChar(int viewId, String methodName, char value)
    {
        addAction(new ReflectionAction(viewId, methodName, ReflectionAction.CHAR, value));
    }
    
    /**
     * Call a method taking one String on a view in the layout for this
     * RemoteViews.
     * 
     * @param viewId
     *            The id of the view whose text should change
     * @param methodName
     *            The name of the method to call.
     * @param value
     *            The value to pass to the method.
     */
    public void setString(int viewId, String methodName, String value)
    {
        addAction(new ReflectionAction(viewId, methodName, ReflectionAction.STRING, value));
    }
    
    /**
     * Call a method taking one CharSequence on a view in the layout for this
     * RemoteViews.
     * 
     * @param viewId
     *            The id of the view whose text should change
     * @param methodName
     *            The name of the method to call.
     * @param value
     *            The value to pass to the method.
     */
    public void setCharSequence(int viewId, String methodName, CharSequence value)
    {
        addAction(new ReflectionAction(viewId, methodName, ReflectionAction.CHAR_SEQUENCE, value));
    }
    
    /**
     * Call a method taking one Uri on a view in the layout for this
     * RemoteViews.
     * 
     * @param viewId
     *            The id of the view whose text should change
     * @param methodName
     *            The name of the method to call.
     * @param value
     *            The value to pass to the method.
     */
    public void setUri(int viewId, String methodName, Uri value)
    {
        addAction(new ReflectionAction(viewId, methodName, ReflectionAction.URI, value));
    }
    
    /**
     * Call a method taking one Bitmap on a view in the layout for this
     * RemoteViews.
     * 
     * @more <p class="note">
     *       The bitmap will be flattened into the parcel if this object is sent
     *       across processes, so it may end up using a lot of memory, and may
     *       be fairly slow.
     *       </p>
     * 
     * @param viewId
     *            The id of the view whose text should change
     * @param methodName
     *            The name of the method to call.
     * @param value
     *            The value to pass to the method.
     */
    public void setBitmap(int viewId, String methodName, Bitmap value)
    {
        addAction(new ReflectionAction(viewId, methodName, ReflectionAction.BITMAP, value));
    }
    
    /**
     * Call a method taking one Bundle on a view in the layout for this
     * RemoteViews.
     * 
     * @param viewId
     *            The id of the view whose text should change
     * @param methodName
     *            The name of the method to call.
     * @param value
     *            The value to pass to the method.
     */
    public void setBundle(int viewId, String methodName, Bundle value)
    {
        addAction(new ReflectionAction(viewId, methodName, ReflectionAction.BUNDLE, value));
    }
    
    public void setViewWidth(int viewId, int value)
    {
        addAction(new SetLayoutSize(viewId, SetLayoutSize.WIDTH, value));
    }
    
    public void setViewHeight(int viewId, int value)
    {
        addAction(new SetLayoutSize(viewId, SetLayoutSize.HEIGHT, value));
    }
    
    /**
     * Inflates the view hierarchy represented by this object and applies all of
     * the actions.
     * 
     * <p>
     * <strong>Caller beware: this may throw</strong>
     * 
     * @param context
     *            Default context to use
     * @param parent
     *            Parent that the resulting view hierarchy will be attached to.
     *            This method does <strong>not</strong> attach the hierarchy.
     *            The caller should do so when appropriate.
     * @return The inflated view hierarchy
     */
    public View apply(Context context, ViewGroup parent)
    {
        View result;
        
        LayoutInflater inflater = LayoutInflater.from(context);
        result = inflater.inflate(mLayoutId, parent);
        performApply(result);
        
        return result;
    }
    
    /**
     * Applies all of the actions to the provided view.
     * 
     * <p>
     * <strong>Caller beware: this may throw</strong>
     * 
     * @param v
     *            The view to apply the actions to. This should be the result of
     *            the {@link #apply(Context,ViewGroup)} call.
     */
    public void reapply(View v)
    {
        performApply(v);
    }
    
    private void performApply(View v)
    {
        try
        {
            if (mActions != null)
            {
                final int count = mActions.size();
                for (int i = 0; i < count; i++)
                {
                    Action a = mActions.get(i);
                    a.apply(v);
                }
            }
        }
        catch (OutOfMemoryError e)
        {
            System.gc();
        }
    }
    
    public int describeContents()
    {
        return 0;
    }
    
    public void writeToParcel(Parcel dest, int flags)
    {
        dest.writeInt(mLayoutId);
        int count;
        if (mActions != null)
        {
            count = mActions.size();
        }
        else
        {
            count = 0;
        }
        dest.writeInt(count);
        for (int i = 0; i < count; i++)
        {
            Action a = mActions.get(i);
            a.writeToParcel(dest, 0);
        }
    }
    
    /**
     * Parcelable.Creator that instantiates RemoteViews objects
     */
    public static final Parcelable.Creator<SimpleRemoteViews> CREATOR = new Parcelable.Creator<SimpleRemoteViews>() {
        public SimpleRemoteViews createFromParcel(Parcel parcel)
        {
            return new SimpleRemoteViews(parcel);
        }
        
        public SimpleRemoteViews[] newArray(int size)
        {
            return new SimpleRemoteViews[size];
        }
    };
}